﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using Storage;
using Storage.EntityModels;
using coord = System.Double;
using System.Runtime.Serialization;
using Microsoft.SqlServer.Types;

namespace GeoRef
{
    /// <summary>
    /// Represents a location, that is a point with an address and/or
    /// a description associated 
    /// </summary>
    [KnownTypeAttribute(typeof(GeoLoc))]
    [KnownTypeAttribute(typeof(GeoPoint))]
    [DataContract]
    public class GeoLoc : GeoPoint
    {
        #region GeoLoc fields
        /// <value>
        /// Description
        /// </value>
        [DataMember]
        private string description;

        /// <value>
        /// Address
        /// </value>
        [DataMember]
        private string address;

        /// <value>
        /// List of users who are getting
        /// in the vehicle at current
        /// location
        /// </value>
        [DataMember]
        private List<int> gettingInUsers;

        /// <value>
        /// List of users who are getting off
        /// the vehicle at the current location
        /// </value>
        [DataMember]
        private List<int> gettingOffUsers;

        /// <value>
        /// Array containing the splitted version
        /// of the address
        /// </value>
        private string[] locations;

        #endregion

        #region GeoLoc Constructors

        /// <summary>
        /// Parameterless costructor ONLY for Webservice use
        /// </summary>
        public GeoLoc()
            : this("0, 0", "", "") {
            gettingInUsers = new List<int>();
            gettingOffUsers = new List<int>();

            ResetLocation();
        }

        /// <summary>
        /// Constructor which creates a location with coordinates called
        /// coordinates, address addr and description descr
        /// </summary>
        /// <param name="coordinates">Coordinate expressed by a string: two
        /// floats separated by a comma</param>
        /// <param name="addr">Address, that can be a null string</param>
        /// <param name="descr">Description, that can be a null string</param>
        /// <example>
        /// <code>
        /// GeoLoc myLoc = new GeoLoc("43.716667,10.4", "Pisa, Italy", "");
        /// </code>
        /// </example>
        /// <seealso cref="GeoRef.GeoPoint"/>
        public GeoLoc(string coordinates, string addr, string descr)
            : base(coordinates) {
            gettingInUsers = new List<int>();
            gettingOffUsers = new List<int>(); 
            this.description = descr;
            this.address = addr;

            ResetLocation();
        }

        /// <summary>
        /// Constructor which creates a location with coordinates lat and lng,
        /// address addr and description descr
        /// </summary>
        /// <param name="lat">Latitude</param>
        /// <param name="lng">Longitude</param>
        /// <param name="addr">Address, that can be null string</param>
        /// <param name="descr">Description, that can be null string</param>
        /// <seealso cref="GeoRef.GeoPoint"/>
        public GeoLoc(coord lat, coord lng, string addr, string descr)
            : base(lat, lng) {
            gettingInUsers = new List<int>();
            gettingOffUsers = new List<int>();            
            this.description = descr;
            this.address = addr;

            ResetLocation();
        }

        /// <summary>
        /// Constructor which creates a location with coordinates described by point,
        /// address addr and description descr
        /// </summary>
        /// <param name="point">Point as GeoPoint object</param>
        /// <param name="addr">Address, that can be null string</param>
        /// <param name="descr">Description, that can be null string</param>
        /// <seealso cref="GeoRef.GeoPoint"/>
        public GeoLoc(GeoPoint point, string addr, string descr)
            : base(point.Lat, point.Lng) {
            gettingInUsers = new List<int>();
            gettingOffUsers = new List<int>();            
            this.description = descr;
            this.address = addr;
            ResetLocation();
        }

        /// <summary>
        /// Constructor which creates a GeoLoc using a <see cref="Storage.EntityModels.Place"/>
        /// as data source
        /// </summary>
        /// <param name="location">The data source</param>
        /// <seealso cref="GeoRef.GeoPoint"/>
        /// <seealso cref="Storage.EntityModels.Place"/>
        public GeoLoc(Place location)
            : base((double)location.MapPoint.Lat, (double)location.MapPoint.Long) {
            gettingInUsers = new List<int>();
            gettingOffUsers = new List<int>();
            ResetLocation();
            this.address = "";
            if (location.Address != null && location.Address != "") {
                this.address = location.Address + ", ";
            }
            if (location.City != null && location.City != "") {
                this.address += location.City;
            }
            if (location.Province != null && location.Province != "") {
                this.address += " " + location.Province + ", ";
            }
            this.address += location.State;
            this.description = (location.Description != null) ? location.Description : "";
        }
        #endregion

        #region GeoLoc Properties
        /// <summary>
        /// Gets or sets a location description.
        /// </summary>
        public string Description {
            set { description = value; }
            get { return description; }
        }

        /// <summary>
        /// Describes the location address
        /// </summary>
        /// <permission cref="System.Security.PermissionSet">
        /// Public Get
        /// Protected Set
        /// </permission>
        //[DataMember]
        public string Address {
            get { return address; }
            set { address = value; }
        }

        /// <summary>
        /// Gets and sets list of users who
        /// getting in a vehicle in the current
        /// location
        /// </summary>
        internal List<int> GettingInUsers {
            get { return gettingInUsers; }
            set { gettingInUsers = value; }
        }

        /// <summary>
        /// Gets and sets list of users who
        /// getting off a vehicle in the current
        /// location
        /// </summary>
        internal List<int> GettingOffUsers {
            get { return gettingOffUsers; }
            set { gettingOffUsers = value; }
        }

        #endregion

        #region GeoLoc Methods
        /// <summary>
        /// Inserts the specific location as a Place
        /// object in the database and returns it.
        /// If toSave is true, it performs also a 
        /// SubmitChange operation, and so it really
        /// store the Place in db.
        /// </summary>
        /// <param name="toSave">Specifies if location must
        /// be really stored in the database or not.</param>
        /// <returns>The corresponding Place</returns>
        public Place SavePlace(bool toSave) {
            this.RoundCoordinates();
            Place dbLocation = StorageManager.Instance.GetPlaceByLatLong(this.Lat, this.Lng);
            if (dbLocation == null) {
                ParseAddress();
                dbLocation = new Place {
                    City = locations[0],
                    Province = locations[1],
                    State = locations[3],
                    Address = locations[2],
                    Description = this.Description,
                    MapPoint = SqlGeography.Point(this.Lat, this.Lng, 4326)
                };
                StorageManager.Instance.Entities.Places.InsertOnSubmit(dbLocation);
                if (toSave == true) {
                    StorageManager.Instance.Entities.SubmitChanges();
                }
            }
            return dbLocation;
        }
     
        /// <summary>
        /// Inserts the specific location as a Place
        /// object in the database and returns it, without
        /// saving it in db.
        /// </summary>
        /// <returns>The corresponding Place</returns>
        public Place SavePlace() {
            return SavePlace(false);
        }

        /// <summary>
        /// Returns the distance between this GeoLoc and the one given as parameter
        /// </summary>
        /// <param name="loc">The GeoLoc about which the distance has to be calculated</param>
        /// <returns>The distance between this GeoLoc and the given one</returns>
        public double Distance(GeoLoc loc)
        {
            double radiusEarth = 6371;
            double latSt, latEnd, lngSt, lngEnd;
            double fi, p;
            //Converts from degrees to radian
            latSt = Math.PI * this.Lat / 180;
            latEnd = Math.PI * loc.Lat / 180;
            lngSt = Math.PI * this.Lng / 180;
            lngEnd = Math.PI * loc.Lng / 180;
            //Calculates angle between two points
            fi = Math.Abs(lngSt - lngEnd);
            p = Math.Acos(Math.Sin(latEnd) * Math.Sin(latSt) + Math.Cos(latEnd) * Math.Cos(latSt) * Math.Cos(fi));
            return p * radiusEarth;
        }

        /// <summary>
        /// Splits an address given as a single string into multiple
        /// fields representing City, Province, Address and Country.
        /// If the given string begins with a '@' character is supposed
        /// to be a coordinate pair and will be returned as is.
        /// </summary>        
        /// <returns>
        /// A 4-element array with, in order, City, Province,
        /// Address and Country/State
        /// </returns>
        /// <remarks>Only Italian cities have province field</remarks>
        private void ParseAddress() {
            //string[] locations = new string[4];
            string[] tmp = new string[4];
            List<string> splittingLoc = new List<string>();

            ResetLocation();
            this.address = this.address.Trim();

            //transform in a compact version these four italian provinces
            address = address.Replace("Forli-Cesena", "FC");
            address = address.Replace("Forlì-Cesena", "FC");
            address = address.Replace("Pesaro E Urbino", "PU");
            address = address.Replace("Genua", "GE");
            address = address.Replace("Reggio nell'Emilia", "RE");
            address = address.Replace("Reggio di Calabria", "RC");
           
            if (this.address.StartsWith("@")) {
                GeoQuery q = new GeoQuery();
                string[] coords = new string[2];
                coords = this.address.Substring(1).Split(',');
                this.address = q.reverseGeoCoding(Double.Parse(coords[0], CultureInfo.InvariantCulture), Double.Parse(coords[1], CultureInfo.InvariantCulture)).address;
                //address is a form like '@'+lat+','+lng
                //locations[2] = this.address;
            } 
            if (address.Contains(',')) {                
                //address is made up of multiple comma-separated parts
                //split address by comma
                splittingLoc.AddRange(this.address.Split(','));
                //state in last position
                locations[3] = splittingLoc.Last().Trim();
                //city and province are found in next-to-last position 
                splittingLoc[splittingLoc.Count - 2] = splittingLoc[splittingLoc.Count - 2].Trim();

                tmp = splittingLoc[splittingLoc.Count - 2].Split(' ');
                if (locations[3].Contains("Italy") == true) { //italian country                                        
                    locations[1] = tmp[tmp.Length - 1]; //province

                    for (int i = 0; i < (tmp.Length - 1); i++) {
                        double res = 0;
                        if (double.TryParse(tmp[i], out res) == false) {
                            locations[0] += tmp[i] + " ";
                        }
                    }
                } else { //foreign country
                    for (int i = 0; i <= (tmp.Length - 1); i++) {
                        double res = 0;
                        if (double.TryParse(tmp[i], out res) == false) {
                            locations[0] += tmp[i] + " ";
                        }
                    }
                }
                if (tmp.Length == 1) { locations[0] = tmp[0]; }
                /*address (e.g. street, avenue, square) can be found if address to parse
                 has more two elements*/
                if (splittingLoc.Count > 2) {
                    locations[2] = splittingLoc[0]; //address
                    if (splittingLoc.Count == 4) {
                        locations[2] += " " + splittingLoc[1]; //number
                    }
                }
            } else {
                //address contains only country
                locations[3] = this.address;
            }            

        }

        /// <summary>
        /// Initializes the locations array
        /// </summary>
        private void ResetLocation() {
            locations = new string[4];
            //City
            locations[0] = "";
            //Province
            locations[1] = "";
            //Address
            locations[2] = "";
            //Country or State
            locations[3] = "";
        }
        #endregion
    }
}
